let s:self = { 'height': -1, 'width': -1, 'position': 0}

function! s:self.GetConsoleName() dict
    return 'Terminal' . string(tabpagenr())
endfunction

function! s:self.GetName() dict
	if exists('t:terminal_open')
        return t:terminal_open
	endif
    if &buftype ==# 'terminal'
        return bufname('%')[1:]
    endif
    if has_key(g:terminal_program, &ft)
        let l:options = g:terminal_program[&ft]
        if type(l:options) == type("")
            return l:options
        endif
        let l:count = len(l:options)
        if l:count == 1
            return l:options[0]
        endif
        if l:count > 1
            let l:choice = inputlist([ 'Select Your Terminal Program:' ]
                        \ + map(copy(l:options), '(v:key+1).". ".v:val')) - 1
            redraw
            if l:choice < 0 || l:choice >= l:count
                throw "Unexpected-input-received"
            else
                return l:options[l:choice]
            endif
        endif
    endif
    if has_key(g:terminal_program, 'default')
        let l:options = g:terminal_program['default']
        if type(l:options) == type([])
            return l:options[0]
        else
            return l:options
        endif
    endif
endfunction

function! s:StringAfter(word, token)
    let l:loc = strridx(a:word, a:token)
    return a:word[(l:loc+1):]
endfunction

function! s:self.GetShortName() dict
    let l:name = self.GetName()
    return split(s:StringAfter(l:name, '/'), ' ')[0]
endfunction

function! s:self.IsVisible() dict
	if bufwinnr(bufnr(self.GetConsoleName())) != -1
		return 1
	else
		return 0
	endif
endfunction

function! s:self.IsHidden() dict
    if bufnr(self.GetConsoleName()) == -1
        return 0
    elseif self.IsVisible() == 1
        return 0
    else
        return 1
    endif
endfunction

function! s:self.SetOptions() dict
    let l:options = get(g:, 'terminal_options', {})
    if has_key(g:terminal_program, &ft) 
        let l:program = g:terminal_program[&ft]
        if type(l:program) ==# type([]) && len(l:program) > 1
            let l:options = l:program[1]
        endif
    else
        if has_key(g:terminal_program, 'default') 
            let l:program = g:terminal_program['default']
            if type(l:program) ==# type([]) && len(l:program) > 1
                let l:options = l:program[1]
            endif
        endif
    endif
    call extend(self, l:options, "force")
    return self 
endfunction

let s:position_commands = ['botright', 'topleft', 'vertical', 'vertical rightbelow']
function! s:self.GetCommand() dict
    call self.SetOptions()
    let cmdline = s:position_commands[self.position] . ' term ++close ' 
    if self.position < 2
        if self.height > 0
            let cmdline = cmdline . '++rows=' . float2nr(self.height) . ' '
        endif
    else
        if self.width > 0
            let cmdline = cmdline . '++cols=' . float2nr(self.width) . ' '
        endif
    endif
    let cmdline = cmdline . self.GetName()
    return cmdline
endfunction

let s:terminal_exit_commands = {'python': 'exit()', 'default': 'exit'}
function! s:self.GetExitCommand(...) abort
    if a:0 == 0
        let l:name = self.GetShortName()
    else
        let l:name = a:1
    end
    let term_exit_cmds = get(g:, 'terminal_exit_commands', s:terminal_exit_commands)
    call extend(term_exit_cmds, s:terminal_exit_commands)
	if has_key(term_exit_cmds, l:name)
		return term_exit_cmds[l:name]
	elseif has_key(term_exit_cmds, 'default')
		return term_exit_cmds['default']
	else
		return 'exit'
	endif
endfunction

function! s:self.CheckExecutable() dict
    let program = split(self.GetName(), ' ')[0]
    if !executable(program)
        echoerr 'The program ' . program . ' is not executable.'
    endif
endfunction

function! s:onBufEnter() abort
    if (winnr("$") == 1 && (&buftype == "terminal") && bufexists(self.GetConsoleName()))
        q!
    endif
endfunction

function! s:self.Open(...) dict
    if a:0 == 0
        unlet! t:terminal_open
        let t:terminal_open = self.GetName()
    else
        let t:terminal_open = join(a:000, ' ')
    endif
    let l:terminal_open = t:terminal_open
    autocmd BufEnter * call <SID>onBufEnter()
    call self.CheckExecutable()
    exe self.GetCommand()
    exe 'file ' . self.GetConsoleName()
    exe 'setlocal noswapfile'
endfunction

function! s:self.GoToWindowForBufferName(name) dict
	if bufwinnr(bufnr(a:name)) != -1
		exe bufwinnr(bufnr(a:name)) . 'wincmd w'
		return 1
	else
		return 0
	endif
endfunction

function! s:self.InsertMode() dict
    if mode() ==# 'n'
        execute "normal! i"
    endif
endfunction

function! s:self.SendKeys(keys) dict
    call term_sendkeys(self.GetConsoleName(), a:keys)
endfunction

function! s:self.LineReturn() dict
    if has('win32')
        return "\r\n"
    else
        return "\n"
    endif
endfunction

function! s:self.Wait(timeout) dict
    call term_wait(self.GetConsoleName(), a:timeout)
endfunction

function! s:self.Close() dict
    if self.IsVisible()
        call self.GoToWindowForBufferName(self.GetConsoleName())
        call self.InsertMode()
        call self.SendKeys("\<C-W>\<C-C>")
        call self.Wait(50)
        if self.IsVisible()
            call self.SendKeys(self.LineReturn())
            call self.Wait(50)
            call self.SendKeys(self.LineReturn())
            call self.Wait(50)
            call self.SendKeys(self.GetExitCommand() . self.LineReturn())
            call self.Wait(50)
            call self.SendKeys("\<CR>")
        endif
    elseif self.IsHidden()
        call self.Unhide()
        call self.Close()
        return
	endif
    " exe bufwinnr(g:repl_target_n) . 'wincmd w'
    unlet t:terminal_open
endfunction


function! s:self.Unhide() dict
    if self.IsHidden()
        call self.SetOptions()
        exe self.position . ' unhide'
    endif
endfunction

function! s:self.Toggle() dict
    if self.IsVisible()
        call self.Close()
    elseif self.IsHidden()
        call self.Unhide()
    else
        try
            call self.Open()
        catch /Unexpected-input-received/
            echom "Unexpected input received, REPL launch abort."
            return
        endtry
        setlocal nonumber
    endif
endfunction

function! api#term#get() abort
    return deepcopy(s:self)
endfunction

function! api#term#open(...) abort
    call call(s:self.Open, a:000)
endfunction

function! api#term#toggle() abort
    call s:self.Toggle()
endfunction
